from data import Data
import copy
import random
random.seed(0)


class Period(object):
    def __init__(self, product_id, route_id, equip_name,
                 start_time, duration_time, end_time):
        self.product_id = product_id
        self.route_id = route_id
        self.equip_name = equip_name
        self.start_time = start_time
        self.duration_time = duration_time
        self.end_time = end_time

    def write_ans(self):
        rows = []
        row = [
            self.product_id,
            self.route_id,
            self.equip_name,
            self.start_time / 60,
            self.duration_time / 60,
            self.end_time / 60
        ]
        row = list(map(str, row))
        rows.append(','.join(row))

        return rows


class Greedy:
    def __init__(self,
                 product_path="data/产品信息.csv",
                 equip_path="data/设备信息.csv",
                 route_path="data/工艺路线.csv",
                 output_path='data/lx_sol.csv',
                 order=None):
        self.data = Data(product_path, equip_path, route_path)
        self.product_dispatchs = []
        self.output_path = output_path
        if order is None:
            self.order = [20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
                        30, 31, 32, 33, 34, 35, 36, 37, 38, 39,
                        0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
                        10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
        else:
            self.order = order
        self.mp = {'A1': 0, 'B1': 1, 'C1': 2, 'D1': 3}
        self.ready_times = 30
        self.T3_machine = dict()
        self.T4_machine = dict()
        self.T5_machine = dict()
        for i in range(len(self.data.equips)):
            if self.data.equips[i].equip_type == 'T-03':
                self.T3_machine[self.data.equips[i].equip_name] = [0, -1]
            if self.data.equips[i].equip_type == 'T-04':
                self.T4_machine[self.data.equips[i].equip_name] = [0, -1]
            if self.data.equips[i].equip_type == 'T-05':
                self.T5_machine[self.data.equips[i].equip_name] = [0, [(0, 0), (100000000, 100000000)]]
        self.product_status = list()
        self.T5_task_list = list()

    def is_finished(self, a):
        for i in range(len(a)):
            if a[i][0] != 2:
                return False
        return True

    def is_exist_unstart(self, a):
        flag = False
        for i in range(len(a)):
            if a[i][0] == 0:
                flag = True
        return flag

    def is_tight(self, now_time, product_time, product_idx, product_step):
        tmp_time = self.get_best_end_time(now_time, product_time, product_idx, product_step)
        duration = 12 * self.data.products[product_idx].section_num
        if tmp_time == now_time + duration:
            return 1
        return 0

    def get_best_end_time(self, now_time, product_time, product_idx, product_step):
        product = self.data.products[product_idx]
        route = self.data.routes[self.mp[product.route_id]]
        B_duration = route.cost_time_low[product_step]
        C_duration = 12 * product.section_num
        early_time = 10000000
        if route.equip_types[product_step] == 'T-03':
            for name in self.T3_machine:
                pp_time = 30
                if self.T3_machine[name][1] == product_idx:
                    pp_time = 0
                early_time = min(early_time, self.T3_machine[name][0] + pp_time)
        else:
            for name in self.T4_machine:
                pp_time = 30
                if self.T4_machine[name][1] == product_idx:
                    pp_time = 0
                early_time = min(early_time, self.T4_machine[name][0] + pp_time)
        time = max(product_time, early_time) + B_duration
        return max(time, now_time) + C_duration
    
    def get_step_delta(self, product_step, product_idx):
        product = self.data.products[product_idx]
        route = self.data.routes[self.mp[product.route_id]]
        return len(route.cost_time_low) - product_step
    
    def find_work_product(self, now_time):
        pos = -1
        tight = 0
        end_time = 100000000
        for i in range(len(self.product_status)):
            if self.product_status[i][0] == 1:
                # 是否紧
                tmp_tight = self.is_tight(now_time, product_time=self.product_status[i][2], 
                                          product_idx=self.product_status[i][3], product_step=self.product_status[i][1])
                # 是否均摊
                # tmp_step_delta = self.get_step_delta(product_step=self.product_status[i][1], product_idx=self.product_status[i][3])
                # 获得最佳结束时间
                tmp_end_time = self.get_best_end_time(now_time, product_time=self.product_status[i][2], 
                                                      product_idx=self.product_status[i][3], product_step=self.product_status[i][1])
                # tmp_end_time = 0
                if tmp_tight > tight or (tmp_tight == tight and tmp_end_time < end_time):
                    pos = i
                    tight = tmp_tight
                    end_time = tmp_end_time
        if pos == -1 or tight == 0:
            tmp_pos = -1
            for i in range(len(self.product_status)):
                if self.product_status[i][0] == 0:
                    tmp_pos = i
                    break
            cnt = 0
            for i in range(len(self.product_status)):
                if self.product_status[i][0] == 0:
                    cnt += 1
            if cnt <= 3:
                for i in range(len(self.product_status)):
                    if self.product_status[i][0] == 0:
                        self.product_status[i][0] = 1
            if tmp_pos != -1:
                self.product_status[tmp_pos][0] = 1
                return tmp_pos
        return pos

    def update_status(self, product_status_idx, now_time):
        product_idx = self.product_status[product_status_idx][3]
        product_time = self.product_status[product_status_idx][2]
        product_step = self.product_status[product_status_idx][1]
        product = self.data.products[product_idx]
        route = self.data.routes[self.mp[product.route_id]]
        B_duration = route.cost_time_low[product_step]
        C_duration = 12 * product.section_num
        early_time = 10000000
        # 找ready时间最早的机器
        if route.equip_types[product_step] == 'T-03':
            for name in self.T3_machine:
                pp_time = 30
                if self.T3_machine[name][1] == product_idx:
                    pp_time = 0
                early_time = min(early_time, self.T3_machine[name][0] + pp_time)
        else:
            for name in self.T4_machine:
                pp_time = 30
                if self.T4_machine[name][1] == product_idx:
                    pp_time = 0
                early_time = min(early_time, self.T4_machine[name][0] + pp_time)
        time = max(product_time, early_time) + B_duration
        B_start_time = max(time, now_time) - B_duration
        B_end_time = max(time, now_time)
        # 找浪费时间最少的机器
        waste_time = 10000000
        machine_name = -1
        if route.equip_types[product_step] == 'T-03':
            for name in self.T3_machine:
                pp_time = 30
                if self.T3_machine[name][1] == product_idx:
                    pp_time = 0
                if self.T3_machine[name][0] + pp_time <= B_start_time:
                    if B_start_time - self.T3_machine[name][0] < waste_time:
                        waste_time = B_start_time - self.T3_machine[name][0]
                        machine_name = name
        else:
            for name in self.T4_machine:
                pp_time = 30
                if self.T4_machine[name][1] == product_idx:
                    pp_time = 0
                if self.T4_machine[name][0] + pp_time <= B_start_time:
                    if B_start_time - self.T4_machine[name][0] < waste_time:
                        waste_time = B_start_time - self.T4_machine[name][0]
                        machine_name = name
        if route.equip_types[product_step] == 'T-03':
            self.T3_machine[machine_name] = [B_end_time, product_idx]
        else:
            self.T4_machine[machine_name] = [B_end_time, product_idx]
        self.product_dispatchs.append(Period(product.product_id, product_step + 1, machine_name, B_start_time, B_duration, B_end_time))
        for i in range(product.section_num):
            self.product_dispatchs.append(Period(product.product_id, product_step + 2, 'Y-2045', B_end_time + 12 * i, 12, B_end_time + 12 * (i + 1)))
        self.product_status[product_status_idx][1] += 2
        self.product_status[product_status_idx][2] = max(time, now_time) + C_duration
        if self.product_status[product_status_idx][1] == len(route.cost_time_low):
            self.product_status[product_status_idx][0] = 2
            print('warning some product goes wrong!')
        elif route.equip_types[self.product_status[product_status_idx][1]] == 'T-05':
            if self.product_status[product_status_idx][1] >= 12:
                self.T5_task_list.append(product_status_idx)
                self.product_status[product_status_idx][0] = 2
            else:
                self.update_D_machine_immediate(product_status_idx)
        return max(time, now_time) + C_duration
    
    def update_D_machine_immediate(self, product_status_idx):
        _, step, time, idx = self.product_status[product_status_idx]
        product = self.data.products[idx]
        route = self.data.routes[self.mp[product.route_id]]
        duration = route.cost_time_low[step]
        final_time = 0
        for i in range(product.section_num):
            machine_name = -1
            start_time = 1000000000
            pos = -1
            for name in self.T5_machine:
                tmp_pos = -1
                for i in range(1, len(self.T5_machine[name][1])):
                    if self.T5_machine[name][1][i][0] - duration >= max(self.T5_machine[name][1][i - 1][1], time):
                        tmp_pos = i
                        break
                if max(self.T5_machine[name][1][tmp_pos - 1][1], time) < start_time:
                    start_time = max(self.T5_machine[name][1][tmp_pos - 1][1], time)
                    machine_name = name
                    pos = tmp_pos
            self.T5_machine[machine_name][0] = start_time + duration
            self.T5_machine[machine_name][1].insert(pos, (start_time, start_time + duration))
            self.product_dispatchs.append(Period(product.product_id, step + 1, machine_name, start_time, duration, start_time + duration))
            final_time = max(final_time, start_time + duration)
        self.product_status[product_status_idx][1] += 1
        self.product_status[product_status_idx][2] = final_time
    
    def update_D_machine(self):
        for product_status_idx in self.T5_task_list:
            self.update_D_machine_immediate(product_status_idx)

    # 按序安排 启发式贪心
    # status 0未开始 1已开始 2结束
    # (status, step, now_time, idx)
    def solve(self):
        for i in range(len(self.order)):
            idx = self.order[i]
            if i % 2 == 0:
                self.product_dispatchs.append(Period(self.data.products[idx].product_id, 1, 'Z-1001', 240 * (i // 2), 240, 240 * ((i // 2) + 1)))
            else:
                self.product_dispatchs.append(Period(self.data.products[idx].product_id, 1, 'Z-1002', 240 * (i // 2), 240, 240 * ((i // 2) + 1)))
            self.product_status.append([0, 1, 240 * ((i // 2) + 1), idx])
        now_time = 0
        while not self.is_finished(self.product_status):
            product_status_idx = self.find_work_product(now_time)
            now_time = self.update_status(product_status_idx, now_time)
        self.update_D_machine()
        self.product_dispatchs.sort(key=lambda x: (x.product_id, x.route_id, x.start_time))
        ans = 0
        for i in range(len(self.product_dispatchs)):
            ans = max(ans, self.product_dispatchs[i].end_time)
        return ans

    def write_ans(self):
        rows = list()
        rows.append('product_id,route_No,equ_name,start,duration,end')
        for i in range(len(self.product_dispatchs)):
            rows += self.product_dispatchs[i].write_ans()
        # for row in rows:
        #     print(row)
        f = open(self.output_path, 'w', encoding='utf-8')
        for row in rows:
            f.write(row + '\n')
        f.close()


class GeneticSolver:
    def __init__(self):
        self.epoch = 200
        self.group_num = 1000
        self.n = 40
        self.relax_coe = 2
        self.mutation_line = 0.6
    
    def init_group(self):
        order = [20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
                 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,
                 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
                 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
        group = list()
        group.append([self.calc(order), order])
        while len(group) < self.group_num:
            new_order = copy.deepcopy(order)
            random.shuffle(new_order)
            group.append([self.calc(new_order), new_order])
        return group
    
    def calc(self, order):
        solver = Greedy(order=order)
        val = solver.solve()
        return val

    def mutation(self, pso):
        x = random.randint(0, self.n - 1)
        y = random.randint(0, self.n - 1)
        if x > y:
            x, y = y, x
        new_pso = copy.deepcopy(pso)
        for i in range(x, (x + y) // 2 + 1):
            new_pso[i], new_pso[x + y - i] = new_pso[x + y - i], new_pso[i]
        return new_pso

    def crossover(self, pso1, pso2):
        new_pso1 = list()
        new_pso2 = list()
        st = set()
        for i in range(self.n // 2):
            st.add(pso1[i])
            new_pso1.append(pso1[i])
        for i in range(self.n):
            if pso2[i] not in st:
                new_pso1.append(pso2[i])
            if pso2[i] in st:
                new_pso2.append(pso2[i])
        for i in range(self.n // 2, self.n):
            new_pso2.append(pso1[i])
        assert len(new_pso1) == self.n and len(new_pso2) == self.n
        return new_pso1, new_pso2

    def solve(self):
        group = self.init_group()
        history_best = 100000
        best_order = None
        for i in range(self.epoch):
            epoch_best = 100000
            epoch_avg = 0
            for j in range(self.group_num):
                val = group[j][0]
                epoch_avg += val
                epoch_best = min(epoch_best, val)
                if val < history_best:
                    history_best = val
                    best_order = group[j][1]
            print('epoch %d: epoch_avg-%.2f epoch_best-%.2f history_best-%.2f' % (i + 1, epoch_avg / self.group_num, epoch_best, history_best))
            new_group = list()
            while len(new_group) < self.relax_coe * self.group_num:
                p = random.random()
                if p < self.mutation_line:
                    pso = self.mutation(group[random.randint(0, len(group) - 1)][1])
                    new_group.append([self.calc(pso), pso])
                else:
                    pso1, pso2 = self.crossover(group[random.randint(0, len(group) - 1)][1], group[random.randint(0, len(group) - 1)][1])
                    new_group.append([self.calc(pso1), pso1])
                    new_group.append([self.calc(pso2), pso2])
            new_group.sort(key=lambda x: x[0])
            group = new_group[:self.group_num]
        return best_order
